package model

import (
	"fmt"
	"sort"
	"strings"
	"time"

	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	httpConn "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
	"google.golang.org/protobuf/proto"
	v1 "k8s.io/api/core/v1"

	meshconfig "istio.io/api/mesh/v1alpha1"
	networking "istio.io/api/networking/v1alpha3"
	. "istio.io/istio/pilot/pkg/config/alikube/ingress/log"
	"istio.io/istio/pilot/pkg/features"
	alifeatures "istio.io/istio/pkg/ali/features"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/constants"
)

type DestinationType string

const (
	Single DestinationType = "Single"

	Multiple DestinationType = "Multiple"
)

type BackendService struct {
	Namespace string
	Name      string
	Port      uint32
	Weight    int32
}

type IngressRoute struct {
	Name            string
	Host            string
	PathType        string
	Path            string
	DestinationType DestinationType
	ServiceName     string
	ServiceList     []BackendService
	Error           string

	IngressNamespacedName  string
	FullRequestURI         string
	UnsupportedAnnotations []string
}

type IngressRouteCollection struct {
	Valid   []IngressRoute
	Invalid []IngressRoute
}

type IngressDomain struct {
	Host string

	// tls for HTTPS
	// default HTTP
	Protocol string

	// cluster id/namespace/name
	SecretName string

	// creation time of ingress resource
	CreationTime time.Time
	Error        string
}

type IngressDomainCollection struct {
	Valid   []IngressDomain
	Invalid []IngressDomain
}

type CheckIngressResponse struct {
	Success            bool
	Reason             string
	IsAllSupported     bool
	IncompatibleRoutes []IngressRoute
}

func SortStableForIngressRoutes(routes []IngressRoute) {
	isAllMatch := func(route IngressRoute) bool {
		return route.PathType == "prefix" && route.Path == "/"
	}

	sort.SliceStable(routes, func(i, j int) bool {
		if routes[i].Host != routes[j].Host {
			return len(routes[i].Host) > len(routes[j].Host)
		}

		if isAllMatch(routes[i]) {
			return false
		}
		if isAllMatch(routes[j]) {
			return true
		}

		if routes[i].PathType == routes[j].PathType {
			// sort canary
			if routes[i].Path == routes[j].Path {
				return strings.HasSuffix(routes[i].Name, "canary")
			}

			return len(routes[i].Path) > len(routes[j].Path)
		}

		if routes[i].PathType == "exact" {
			return true
		}

		if routes[i].PathType != "exact" &&
			routes[j].PathType != "exact" {
			return routes[i].PathType == "prefix"
		}

		return false
	})
}

// IngressStore provide uniform access to get convert resources from ingress for mse ops via debug interface.
type IngressStore interface {
	GetIngressRoutes() IngressRouteCollection

	GetIngressDomains() IngressDomainCollection

	CheckIngress(clusterName string) CheckIngressResponse

	Services(clusterName string) ([]*v1.Service, error)

	IngressControllers() map[string]string
}

func CreateCRName(host string) string {
	return strings.Join([]string{features.ClusterName, host}, "-")
}

func CreateMatchingGWName(host string) string {
	return fmt.Sprintf("%s/%s", alifeatures.WatchResourcesByNamespaceForPrimaryCluster, CreateCRName(host))
}

// VirtualServiceFilter will modify copied configs from underlying store.
// We merge routes into pre host of virtual service.
func VirtualServiceFilter(configs []config.Config) []config.Config {
	if alifeatures.WatchResourcesByNamespaceForPrimaryCluster == "" {
		return configs
	}

	autoGenerated := make(map[string]*config.Config, len(configs))
	configsForName := make(map[string]*config.Config, len(configs))
	var dedicatedConfigs []config.Config
	var out []config.Config

	globalGWName := CreateMatchingGWName("*")

	for idx := range configs {
		c := configs[idx]
		virtualService := c.Spec.(*networking.VirtualService)
		if strings.HasPrefix(c.Name, constants.IstioMcpAutoGeneratedVsName) {
			out = append(out, c)
			continue
		}
		if strings.HasPrefix(c.Name, constants.IstioIngressGatewayName) {
			autoGenerated[strings.TrimPrefix(c.Name, constants.IstioIngressGatewayName+"-")] = &c
		} else if strings.HasSuffix(c.Name, "envoy-waf-global") {
			virtualService.Gateways = []string{globalGWName}
			dedicatedConfigs = append(dedicatedConfigs, c)
		} else {
			host := obtainHostFromVS(virtualService)
			virtualService.Gateways = []string{CreateMatchingGWName(host)}
			if host != "*" {
				virtualService.Gateways = append(virtualService.Gateways, globalGWName)
			}
			configsForName[host] = &c
		}
	}

	IngressLog.Infof("Auto generator virtual services number %d", len(autoGenerated))

	for host, c := range autoGenerated {
		opsVS, exist := configsForName[host]
		if exist {
			vs := opsVS.Spec.(*networking.VirtualService)
			autoGeneratedVS := c.Spec.(*networking.VirtualService)
			if len(vs.HostHTTPFilters) == 0 {
				vs.HostHTTPFilters = autoGeneratedVS.HostHTTPFilters
			}
			// TODO(special.fy) make configurable for priority of routes between OPS, ACK and ASM
			vs.Http = append(vs.Http, autoGeneratedVS.Http...)
		} else {
			configsForName[host] = c
		}
	}

	for host, c := range configsForName {
		IngressLog.Debugf("host: %s, config: %v", host, c)
		if c.Annotations == nil {
			c.Annotations = map[string]string{}
		}
		c.Annotations[constants.MSEOriginName] = c.Name
		c.Name = CreateCRName(host)
		out = append(out, *c)
	}
	out = append(out, dedicatedConfigs...)
	return out
}

// DestinationFilter will modify copied configs from underlying store.
func DestinationFilter(configs []config.Config) []config.Config {
	if alifeatures.WatchResourcesByNamespaceForPrimaryCluster == "" {
		return configs
	}

	var autoGenerated []*config.Config
	configsForName := make(map[string]*config.Config, len(configs))

	for idx := range configs {
		c := configs[idx]
		if strings.HasPrefix(c.Name, constants.IstioIngressGatewayName) {
			autoGenerated = append(autoGenerated, &c)
		} else {
			configsForName[c.Name] = &c
		}
	}

	IngressLog.Infof("Auto generator destination rule number %d", len(autoGenerated))

	for _, c := range autoGenerated {
		// DestinationRule name of ops is md5 without cluster id.
		targetName := strings.TrimPrefix(c.Name, constants.IstioIngressGatewayName+"-")
		_, exist := configsForName[targetName]
		if !exist {
			// We change the auto-generated config name to the format of cr name same with ops when ops
			// don't have destination rule for this service.
			c.Name = targetName
			configsForName[targetName] = c
		}
	}

	var out []config.Config
	for _, c := range configsForName {
		out = append(out, *c)
	}
	return out
}

func obtainHostFromGW(gateway *networking.Gateway) string {
	for _, server := range gateway.Servers {
		for _, host := range server.Hosts {
			return host
		}
	}

	return "*"
}

func obtainHostFromVS(virtualService *networking.VirtualService) string {
	for _, host := range virtualService.Hosts {
		return host
	}

	return "*"
}

// GatewayFilter will modify copied configs from underlying store.
// We merge routes into pre host of virtual service.
func GatewayFilter(configs []config.Config) []config.Config {
	if alifeatures.WatchResourcesByNamespaceForPrimaryCluster == "" {
		return configs
	}

	autoGenerated := make(map[string]*config.Config, len(configs))
	configsForName := make(map[string]*config.Config, len(configs))

	for idx := range configs {
		c := configs[idx]
		if strings.HasPrefix(c.Name, constants.IstioIngressGatewayName) {
			autoGenerated[strings.TrimPrefix(c.Name, constants.IstioIngressGatewayName+"-")] = &c
		} else {
			gateway := c.Spec.(*networking.Gateway)
			configsForName[obtainHostFromGW(gateway)] = &c
		}
	}

	IngressLog.Infof("Auto generator gateways number %d", len(autoGenerated))

	// Add ingress defined host but not defined in ops
	for host, c := range autoGenerated {
		_, exist := configsForName[host]
		if !exist {
			configsForName[host] = c
		}
	}

	var out []config.Config
	for host, c := range configsForName {
		c.Name = CreateCRName(host)
		out = append(out, *c)
	}
	return out
}

func (ps *PushContext) GetGatewayByName(name string) *config.Config {
	parts := strings.Split(name, "/")
	if len(parts) != 2 {
		return nil
	}

	for _, cfg := range ps.gatewayIndex.all {
		if cfg.Namespace == parts[0] && cfg.Name == parts[1] {
			return &cfg
		}
	}

	return nil
}

func (ps *PushContext) GetHTTPFiltersFromEnvoyFilter(node *Proxy) []*httpConn.HttpFilter {
	var out []*httpConn.HttpFilter
	envoyFilterWrapper := ps.EnvoyFilters(node)
	if envoyFilterWrapper != nil && len(envoyFilterWrapper.Patches) > 0 {
		httpFilters := envoyFilterWrapper.Patches[networking.EnvoyFilter_HTTP_FILTER]
		if len(httpFilters) > 0 {
			for _, filter := range httpFilters {
				if filter.Operation == networking.EnvoyFilter_Patch_INSERT_AFTER ||
					filter.Operation == networking.EnvoyFilter_Patch_ADD ||
					filter.Operation == networking.EnvoyFilter_Patch_INSERT_BEFORE ||
					filter.Operation == networking.EnvoyFilter_Patch_INSERT_FIRST {
					out = append(out, proto.Clone(filter.Value).(*httpConn.HttpFilter))
				}
			}
		}
	}

	return out
}

func (ps *PushContext) GetExtensionConfigsFromEnvoyFilter(node *Proxy) []*core.TypedExtensionConfig {
	var out []*core.TypedExtensionConfig
	envoyFilterWrapper := ps.EnvoyFilters(node)
	if envoyFilterWrapper != nil && len(envoyFilterWrapper.Patches) > 0 {
		extensionConfigs := envoyFilterWrapper.Patches[networking.EnvoyFilter_EXTENSION_CONFIG]
		if len(extensionConfigs) > 0 {
			for _, extension := range extensionConfigs {
				if extension.Operation == networking.EnvoyFilter_Patch_ADD {
					out = append(out, proto.Clone(extension.Value).(*core.TypedExtensionConfig))
				}
			}
		}
	}

	return out
}

func mergeProxyConfigWhenNeeded(dest *meshconfig.ProxyConfig, src *meshconfig.ProxyConfig) {
	if src != nil {
		dest.DisableAlpnH2 = src.DisableAlpnH2
	}
}

func enableH3(push *PushContext) bool {
	if push == nil || push.Mesh == nil || push.Mesh.MseIngressGlobalConfig == nil {
		return false
	}

	return push.Mesh.MseIngressGlobalConfig.EnableH3
}
